// https://github.com/DeepElement/markdown-it-plantuml-offline
import { debounce } from 'lodash-es'
import pako from 'pako'
import { defineComponent, h, nextTick, onMounted, reactive, ref, watch } from 'vue'
import Markdown from 'markdown-it'
import Token from 'markdown-it/lib/token'
import { Plugin } from '@fe/context'

const emptySrc = ''

const baseUrl = location.origin + '/api/plantuml/png'

const MarkdownItPlugin = function umlPlugin (md: Markdown, options: any) {
  function generateSourceDefault (umlCode: string) {
    if (!umlCode.trim()) {
      return ''
    }

    const data = btoa(
      pako.deflateRaw(umlCode).reduce((d, b) => d + String.fromCharCode(b), '')
    )

    return baseUrl + '?data=' + encodeURIComponent(data)
  }

  options = options || {}

  const openMarker = options.openMarker || '@startuml'
  const openChar = openMarker.charCodeAt(0)
  const closeMarker = options.closeMarker || '@enduml'
  const closeChar = closeMarker.charCodeAt(0)
  const render = options.render || md.renderer.rules.image
  const generateSource = options.generateSource || generateSourceDefault

  function uml (state: any, startLine: any, endLine: any, silent: any) {
    let nextLine
    let i
    let autoClosed = false
    let start = state.bMarks[startLine] + state.tShift[startLine]
    let max = state.eMarks[startLine]

    // Check out the first character quickly,
    // this should filter out most of non-uml blocks
    //
    if (openChar !== state.src.charCodeAt(start)) { return false }

    // Check out the rest of the marker string
    //
    for (i = 0; i < openMarker.length; ++i) {
      if (openMarker[i] !== state.src[start + i]) { return false }
    }

    const markup = state.src.slice(start, start + i)
    const params = state.src.slice(start + i, max)

    // Since start is found, we can report success here in validation mode
    //
    if (silent) { return true }

    // Search for the end of the block
    //
    nextLine = startLine

    for (;;) {
      nextLine++
      if (nextLine >= endLine) {
        // unclosed block should be autoclosed by end of document.
        // also block seems to be autoclosed by end of parent
        break
      }

      start = state.bMarks[nextLine] + state.tShift[nextLine]
      max = state.eMarks[nextLine]

      if (start < max && state.sCount[nextLine] < state.blkIndent) {
        // non-empty line with negative indent should stop the list:
        // - ```
        //  test
        break
      }

      if (closeChar !== state.src.charCodeAt(start)) {
        // didn't find the closing fence
        continue
      }

      if (state.sCount[nextLine] > state.sCount[startLine]) {
        // closing fence should not be indented with respect of opening fence
        continue
      }

      let closeMarkerMatched = true
      for (i = 0; i < closeMarker.length; ++i) {
        if (closeMarker[i] !== state.src[start + i]) {
          closeMarkerMatched = false
          break
        }
      }

      if (!closeMarkerMatched) {
        continue
      }

      // make sure tail has spaces only
      if (state.skipSpaces(start + i) < max) {
        continue
      }

      // found!
      autoClosed = true
      break
    }

    const contents = state.src
      .split('\n')
      .slice(startLine, nextLine + 1)
      .join('\n')

    // We generate a token list for the alt property, to mimic what the image parser does.
    const altToken: any = []
    // Remove leading space if any.
    const alt = params ? params.slice(1) : 'uml diagram'
    state.md.inline.parse(
      alt,
      state.md,
      state.env,
      altToken
    )

    const token = state.push('uml_diagram', 'img', 0)
    // alt is constructed from children. No point in populating it here.

    const imgSrc = generateSource(contents, options)
    token.attrs = [
      ['src', imgSrc],
      ['alt', '']
    ]
    token.block = true
    token.children = altToken
    token.info = params
    token.map = [startLine, nextLine]
    token.markup = markup

    state.line = nextLine + (autoClosed ? 1 : 0)

    return true
  }

  md.block.ruler.before('fence', 'uml_diagram', uml, {
    alt: ['paragraph', 'reference', 'blockquote', 'list']
  })
  md.renderer.rules.uml_diagram = render
}

const Plantuml = defineComponent({
  name: 'plantuml-image',
  props: {
    attrs: Object,
    src: String
  },
  setup (props) {
    const style = reactive({
      backgroundColor: 'transparent',
      backgroundImage: 'url()',
      backgroundPosition: 'center',
      backgroundRepeat: 'no-repeat',
      width: '32px',
      height: '32px'
    })

    const src = ref('')

    const img = ref<HTMLImageElement>()
    const onLoad = () => {
      style.width = 'auto'
      style.height = 'auto'
    }

    const load = debounce((val: string) => {
      src.value = emptySrc
      setTimeout(() => {
        src.value = val
      }, 0)
    }, 1000)

    watch(() => props.src, val => {
      load(val || '')
    })

    onMounted(() => {
      src.value = emptySrc
      nextTick(() => {
        src.value = props.src || ''
      })
    })

    return () => {
      if (img.value && src.value === emptySrc) {
        style.width = img.value.clientWidth + 'px'
        style.height = img.value.clientHeight + 'px'
      }

      return h('img', {
        ...props.attrs,
        ref: img,
        alt: 'plantuml',
        class: 'plantuml-img',
        style,
        src: src.value,
        onLoad
      })
    }
  }
})

function render (tokens: Token[], idx: number) {
  const token = tokens[idx]
  return h(Plantuml, { attrs: token.meta?.attrs, src: token.attrGet('src') || emptySrc })
}

export default {
  name: 'markdown-plantuml',
  register: ctx => {
    const markers = [{
      openMarker: '@startuml',
      closeMarker: '@enduml',
    }, {
      openMarker: '@startsalt',
      closeMarker: '@endsalt',
    }, {
      openMarker: '@startmindmap',
      closeMarker: '@endmindmap',
    }, {
      openMarker: '@startgantt',
      closeMarker: '@endgantt',
    }, {
      openMarker: '@startwbs',
      closeMarker: '@endwbs',
    }, {
      openMarker: '@startjson',
      closeMarker: '@endjson',
    }, {
      openMarker: '@startyaml',
      closeMarker: '@endyaml',
    }]
    markers.forEach((marker) => {
      ctx.markdown.registerPlugin(MarkdownItPlugin, { render, ...marker })
    })

    ctx.registerHook('VIEW_ON_GET_HTML_FILTER_NODE', async ({ node, options }) => {
      const srcAttr = node.getAttribute('src')
      if (srcAttr?.startsWith(baseUrl)) {
        node.removeAttribute('style')

        if (options.preferPng || options.inlineLocalImage) {
          try {
            const res: Response = await ctx.api.fetchHttp(srcAttr)
            const base64Url = await ctx.utils.fileToBase64URL(await res.blob())
            node.setAttribute('src', base64Url)
          } catch (error) {
            console.log(error)
          }
        }
      }
    })
  }
} as Plugin
